package security

import (
	"context"
	"fmt"
	"regexp"
	"strings"
	"sync"
)

const (
	RequestMapUrlSingleWildcard          = "/*"
	RequestMapUrlDoubleWildcard          = "/**"
	RequestMapDenyAll                    = "denyAll"
	RequestMapPermitAll                  = "permitAll"
	RequestMapIsAuthenticatedPrefix      = "isAuthenticated"
	RequestMapIsAuthenticated            = RequestMapIsAuthenticatedPrefix + "()"
	RequestMapIsFullyAuthenticatedPrefix = "isFullyAuthenticated"
	RequestMapIsFullyAuthenticated       = RequestMapIsFullyAuthenticatedPrefix + "()"
	RequestMapHasRolePrefix              = "hasRole"
	RequestMapHasAnyRolePrefix           = "hasAnyRole"
	RequestMapHasNoRolePrefix            = "hasNoRole"
	RequestMapHasNoAnyRolePrefix         = "hasNoAnyRole"
)

type RequestMap struct {
	ID              IDField
	ConfigAttribute string
	HTTPMethod      *string
	URL             string
}

type RequestMapQuery interface {
	QueryAll(ctx context.Context) ([]RequestMap, error)
}

var cachedRequestMaps []RequestMap
var cachedRequestMapsLock sync.RWMutex

func GetCachedRequestMaps(ctx context.Context, query RequestMapQuery) ([]RequestMap, error) {
	cachedRequestMapsLock.RLock()
	isEmpty := len(cachedRequestMaps) == 0

	// IMPORTANT: RUnlock immediately because ReloadCachedRequestMaps() uses write lock inside.
	// This is to avoid deadlock.
	cachedRequestMapsLock.RUnlock()

	if isEmpty {
		if err := ReloadCachedRequestMaps(ctx, query); err != nil {
			return nil, err
		}
	}

	// Reacquires for reading final data
	cachedRequestMapsLock.RLock()
	defer cachedRequestMapsLock.RUnlock()

	return cachedRequestMaps, nil
}

func ReloadCachedRequestMaps(ctx context.Context, query RequestMapQuery) error {
	cachedRequestMapsLock.Lock()
	defer cachedRequestMapsLock.Unlock()

	var err error
	cachedRequestMaps, err = query.QueryAll(ctx)

	return err
}

// Returns the expression like hasRole('ROLE_ADMIN').
// roleAuthority is the string of role authority. AKA role name.
func RequestMapHasRole(roleAuthority string) string {
	return requestMapBuildExpressionWithRole(RequestMapHasRolePrefix, roleAuthority)
}

// Returns the expression like hasAnyRole('ROLE_USER','ROLE_ADMIN').
// roleAuthorities is the list string of role authority. AKA role name.
func RequestMapHasAnyRole(roleAuthorities []string) string {
	return requestMapBuildExpressionWithRoles(RequestMapHasAnyRolePrefix, roleAuthorities)
}

// Returns the expression like hasNoRole('ROLE_ADMIN').
// roleAuthority is the string of role authority. AKA role name.
func RequestMapHasNoRole(roleAuthority string) string {
	return requestMapBuildExpressionWithRole(RequestMapHasNoRolePrefix, roleAuthority)
}

// Returns the expression like hasNoAnyRole('ROLE_USER','ROLE_ADMIN').
// roleAuthorities is the list string of role authority. AKA role name.
func RequestMapHasNoAnyRole(roleAuthorities []string) string {
	return requestMapBuildExpressionWithRoles(RequestMapHasNoAnyRolePrefix, roleAuthorities)
}

// Checks if security request map configAttribute contains any role in roleNames.
func ContainsAnyRoleInConfigAttribute(roleNames []string, configAttribute string) bool {
	return ContainsAnyRoleInConfigRoleNames(roleNames, ExtractRoleNames(configAttribute))
}

// Checks if configuredRoleNames contains any role in roleNames.
func ContainsAnyRoleInConfigRoleNames(roleNames []string, configRoleNames []string) bool {
	containsAnyRole := false

	for _, roleName := range roleNames {
		for _, configRoleName := range configRoleNames {
			if strings.EqualFold(strings.ToLower(roleName), strings.ToLower(configRoleName)) {
				containsAnyRole = true
				break
			}
		}
	}

	return containsAnyRole
}

// Extracts role names from request map config attribute.
// Example: hasRole('admin'), hasAnyRole('reporter', 'planner').
func ExtractRoleNames(configAttribute string) []string {
	var roleNames []string

	// This would match the role names, like 'admin' or 'reporter', 'planner'.
	pattern := fmt.Sprintf(`(?:%v|%v|%v|%v)\((.+)\)`, RequestMapHasRolePrefix, RequestMapHasAnyRolePrefix, RequestMapHasNoRolePrefix, RequestMapHasNoAnyRolePrefix)
	matchedStrings := regexp.MustCompile(pattern).FindStringSubmatch(configAttribute)
	//matchedStrings := regexp.MustCompile(`\w+\((.+)\)`).FindStringSubmatch(configAttribute)

	// If match found.
	if len(matchedStrings) == 2 {
		// Removes all single quotes.
		matchedRoles := strings.Replace(matchedStrings[1], "'", "", -1)

		// Removes all double quotes.
		matchedRoles = strings.Replace(matchedRoles, "\"", "", -1)

		// Splits the string by comma.
		splitedRoles := strings.Split(matchedRoles, ",")

		// Trims space for each role name.
		for _, roleName := range splitedRoles {
			roleNames = append(roleNames, strings.TrimSpace(roleName))
		}
	}

	return roleNames
}

// Returns the expression like hasRole('ROLE_ADMIN'), hasNoRole('ROLE_ADMIN').
// roleAuthority is the string of role authority. AKA role name.
func requestMapBuildExpressionWithRole(prefix string, roleAuthority string) string {
	return fmt.Sprintf("%v('%v')", prefix, roleAuthority)
}

// Returns the expression like hasAnyRole('ROLE_USER','ROLE_ADMIN'), hasNoAnyRole('ROLE_USER','ROLE_ADMIN').
// roleAuthorities is the list string of role authority. AKA role name.
func requestMapBuildExpressionWithRoles(prefix string, roleAuthorities []string) string {
	// The initial string of the expression.
	result := fmt.Sprintf("%v(", prefix)

	// Goes through the list of roleAuthorities and appends it to result.
	for _, authority := range roleAuthorities {
		result += fmt.Sprintf("'%v',", authority)
	}

	// Removes the last comma and closes with closing bracket.
	result = result[:len(result)-1] + ")"

	return result
}
